<?php 

/**
 * sfValidatorNumeroTC valida un numero de tarjeta de credito; mas no determina si la cuenta existe o si mantiene
 * fondos suficientes. Tambien convierte el valor de entrada a un numero entero.
 *
 * @package    AltargetEcommerce
 * @subpackage validator
 * @author     Carlo Lopez <carlo.xavier.lopez 'at' gmail 'dot' com>
 */
class altargetValidatorNumeroTC extends sfValidatorBase
{

  /**
   * Configura el validador.
   *
   * Opciones disponibles:
   *
   * * tipos_tarjeta: los tipos de tarjeta que se aceptaran.
   *
   * OJO: se deben indicar los tipos de tarjeta aceptados, de lo contrario resultara invalido.
   *
   * Codigos de error disponibles:
   *
   * * no_aceptada: el numero es valido pero no pertenece a ningun tipo de tarjeta aceptado.
   * * invalido:    el numero de tarjeta es invalido.
   * 
   * @param array $options   Las opciones
   * @param array $messages  Los mensajes de error
   *
   * @see sfValidatorBase
   */
  protected function configure($options = array(), $messages = array())
  {
    $this->addMessage('no_aceptada', 'El numero "%value%" pertenece a un tipo de tarjeta no aceptado.');
    $this->addMessage('invalido', 'El numero "%value%" es invalido. (ingrese el numero cuidadosamente)');

    $this->addOption('tipos_tarjeta');
    $this->setOption('empty_value', '');
  }

  /**
   * @see sfValidatorBase
   */
  protected function doClean($value)
  {
    $trimmed =  str_replace(array(" ", "-"), "", $value);
    
    if( !$this->validarLuhn($trimmed) )
      throw new sfValidatorError($this, 'invalido', array('value' => $trimmed));
 
    if( !$this->numeroAceptado($trimmed) )
      throw new sfValidatorError($this, 'no_aceptado', array('value' => $trimmed));

    return $trimmed;
  }

  /**
   * Informa si el numero de tarjeta es valido segun el algoritmo de Luhn.
   *
   * @param string $numero Representacion en texto del numero que se quiere validar.
   *
   * @return boolean Devuelve true en caso de ser valido, y false en caso contrario.
   */
  protected function validarLuhn($numero)
  {
    $numlen = strlen($numero);
    $digits = array();
    for($i = ($numlen-1); $i >= 0 ; $i--)
    {
      if(0 == (($numlen - $i) % 2))
        $digits[$i] = 2*$numero[$i];
      else 
        $digits[$i] = $numero[$i];

      if($digits[$i] > 9) 
        $digits[$i] -= 9;
    }
    $sum = 0;
    foreach($digits as $digit)
      $sum += $digit;

    return ((($sum % 10) == 0) && ($sum != 0));

  }

  /**
   * Informa si el numero de tarjeta es aceptado, segun los tipos provistos.
   *
   * *** codigo tomado de "http://www.codepost.org/browse/all/88" ****
   *
   * @param string $numero Representacion en texto del numero que se quiere validar.
   *
   * @return boolean Devuelve true en caso de ser valido, y false en caso contrario.
   *
   * TODO: seria bueno mover la especificacion de los tipos de tarjeta aceptados
   *       a un archivo de configuracion.
   *
   */
  public function numeroAceptado($number)
  {
    if(preg_match('^5[1-5][0-9]{14}^', $number))
        return true; //"MasterCard";
    if(preg_match('^6011[0-9]{12}^', $number))
        return true; //"Discover";
    if(preg_match('^4[0-9]{15}|4[0-9]{12}^', $number))
        return true; //"VISA";
    if(preg_match('^3[0-9]{15}|(2131|1800)[0-9]{11}^', $number))
        return true; //"Japan Credit Bureau (JCB)";
    if(preg_match('^3[47][0-9]{13}^', $number))
        return true; //"AMEX";
    if(preg_match('^(2014|2149)[0-9]{11}^', $number))
        return true; //"enRoute";
    if(preg_match('^8699[0-9]{11}^', $number))
        return true; //"Voyager";
    if(preg_match('^30[0-5][0-9]{11}|(36|38)[0-9]{12}^', $number))
        return true; //"Diners Club / Carte Blanche";

    return false;
  }

}

